/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @file mod_pyhp.c
 * Main apache module
 */

#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <libgen.h>

#include <httpd.h>
#include <http_config.h>
#include <util_filter.h>
#include <http_log.h>
#include <apr_tables.h>
#include <apr_strings.h>

#include "parse.h"
#include "pyhp_layer.h"
#include "request.h"
#include "config.h"
#include "cookies.h"
#include "session.h"

#include "mod_pyhp.h"

#include <node.h> // from python


/**
 * Utility function to append string values to a python dictionary
 */
void pyhp_pydict_add_string(PyObject * d, const char * key, const char * val)
{
    if (!val)
        return;

    PyObject * str = PyString_FromString(val);
    PyDict_SetItemString(d, key, str);
    Py_XDECREF(str);
}


/**
 * Executes a parsed code node as being called from the specified filename
 *
 * @param n The node to execute
 * @param filename The name to set for the execution file
 * @param globals The global environment
 * @param locals The local environment
 * @param flags The flags to set for execution
 *
 * @return The execution result
 */

static PyObject *run_node(struct _node *n, const char *filename,
                          PyObject *globals, PyObject *locals,
                          PyCompilerFlags *flags)
{
    PyCodeObject *co;
    PyObject *v;
    co = PyNode_Compile(n, filename);
    PyNode_Free(n);
    if (co == NULL)
        return NULL;
    v = PyEval_EvalCode(co, globals, locals);
    Py_DECREF(co);
    return v;
}

/**
 * Executes python code reporting it as being loaded from the specified file
 *
 * @param str The code to execute
 * @param filename The filename to set for the executed code
 * @param start The python execution type
 * @param globals The global environment
 * @param locals The local environment
 *
 * @return The Execution result
 */

static PyObject* PyHPRun_StringFileName(const char *str, const char* filename,
                                        int start, PyObject *globals,
                                        PyObject *locals)
{
    struct _node* n = PyParser_SimpleParseString(str, start);
    if (!n)
        return NULL;
    return run_node(n, filename, globals, locals, 0);
}


/**
 * Execute the given python code saving it to a temporary file if in
 * development mode.
 * Report execution errors as choosen by pyhp configuration
 *
 * @param pydata PyHP environment
 * @param r The request for which to execute the code
 * @param code The code to execute
 *
 * @return The execution result, should be OK
 */

int PyHP_RunPython(PyHPData * pydata, request_rec *r, char const * code)
{
    int rv = HTTP_OK;
    PyObject * res;
    char *filename = r->filename;
    int devel = pyhp_config_isdevel(r);
    PyHPLogLocation log_location = pyhp_config_loglocation(r);

    if (!code) return HTTP_INTERNAL_SERVER_ERROR;

    pyhp_layer_set_object(pydata, "status", PyInt_FromLong(rv));

    /* If we are in development mode, create a temporary file for precise error
       reporting */
    if (devel) {
        filename = tmpnam(NULL);
        FILE * f = fopen(filename, "w");
        fwrite(code, 1, strlen(code), f);
        fclose(f);
        res = PyHPRun_StringFileName(code, filename, Py_file_input,
                                     pydata->pyhp_globals,
                                     pydata->pyhp_globals);
    } else
        res = PyRun_String(code, Py_file_input, pydata->pyhp_globals,
                           pydata->pyhp_globals);

    rv = PyInt_AsLong(pyhp_layer_get_object(pydata, "status"));

    /* If execution went well, just dealloc result */
    if(res)
        Py_XDECREF(res);
    else if(PyErr_Occurred()) {
        PyObject *m = NULL;
        PyObject *result = NULL;
        PyObject *type = NULL;
        PyObject *value = NULL;
        PyObject *traceback = NULL;

        rv = HTTP_INTERNAL_SERVER_ERROR;

        PyErr_Fetch(&type,  &value, &traceback);
        PyErr_NormalizeException(&type, &value, &traceback);

        if (!value) {
            value = Py_None;
            Py_INCREF(value);
        }

        if (!traceback) {
            traceback = Py_None;
            Py_INCREF(traceback);
        }

        m = PyImport_ImportModule("traceback");
        if (m) {
            PyObject *d = NULL;
            PyObject *o = NULL;
            d = PyModule_GetDict(m);
            o = PyDict_GetItemString(d, "print_exception");
            if (o) {
                PyObject *args = NULL;

                Py_INCREF(o);
                args = Py_BuildValue("(OOOO)", type, value, traceback,
                                        Py_None);
                if (log_location == PyHPLog_Web)
                    ap_rputs("\n<div class=\"traceback\">\n<pre>", r);

                result = PyEval_CallObject(o, args);

                if (log_location == PyHPLog_Web)
                    ap_rputs("</pre></div>\n", r);

                Py_DECREF(args);
                Py_DECREF(o);
            }
        }

        if (!result) {
            PyErr_Restore(type, value, traceback);
            if (!PyErr_ExceptionMatches(PyExc_SystemExit)) {
                PyErr_Print();
                PyErr_Clear();
            }
            else {
                PyErr_Clear();
            }
        }
        else {
            Py_XDECREF(type);
            Py_XDECREF(value);
            Py_XDECREF(traceback);
        }

        Py_XDECREF(result);
        Py_XDECREF(m);
    }

    //Unlink the temporary file after error reporting
    if (devel)
        unlink(filename);

    return rv;
}

static PyHPData pyhp_data_global;

/**
 * Init hook
 * Initialize the python interpreter and store the instantiated pyhp class
 */

static void pyhp_child_init(apr_pool_t *p, server_rec *s)
{
    srand(time(NULL));
    Py_InitializeEx(0);

    pyhp_data_global.interpreter = Py_NewInterpreter();
    pyhp_data_global.pyhp_layer = pyhp_layer_init();

    pyhp_data_global.sys_module = PyImport_ImportModule("sys");
    pyhp_data_global.pickle_module = PyImport_ImportModule("pickle");

    pyhp_layer_set_internal(&pyhp_data_global, "pydata", &pyhp_data_global);
}

/**
 * Requests handler
 * Accept application/x-httpd-pyhp requests, parse the file, feed the
 * python interpreter.
 */

static int pyhp_handler(request_rec *r)
{
    struct stat stat_result;
    char *file_dir;

    char *result;
    PyHPData *pydata_global, *pydata;
    pyhp_config *config;
    pyhp_config old_config;
    int retv;
    apr_pool_t *pool = r->pool;
    PyObject *old_modules = NULL;
    const char * default_content_type = "text/html";

    if ( (!r->content_type) ||
         (strcmp(r->content_type, "application/x-httpd-pyhp" )) )
        return DECLINED;

    ap_set_content_type(r, default_content_type);

    if(r->header_only)
        return OK;

    /* File check and move to directory */
    if (stat(r->filename, &stat_result))
        return HTTP_NOT_FOUND;

    /* Init interpreter for the request */
    pydata = apr_palloc(pool, sizeof(PyHPData));
    memcpy(pydata, &pyhp_data_global, sizeof(PyHPData));

    apr_pool_userdata_set(pydata, "pyhp_data", NULL, r->pool);
    pydata->pyhp_globals = pyhp_layer_create_globals(pydata->pyhp_layer);

    file_dir = dirname(apr_pstrdup(r->pool, r->filename));
    chdir(file_dir);

    pyhp_layer_set_internal(pydata, "apache_request", r);
    pyhp_layer_set_internal(pydata, "apache_pool", r->pool);

    /* Save current configuration */
    config = pyhp_get_config(r);
    memcpy(&old_config, config, sizeof(pyhp_config));

    /* Replace stdout e stderr with pyhp ones */
    PyObject *sys_mdict = PyModule_GetDict(pydata->sys_module);
    PyObject *pyhp_mdict = PyModule_GetDict(pydata->pyhp_layer);
    PyDict_SetItemString(sys_mdict, "stdout", pydata->pyhp_layer);
    PyDict_SetItemString(sys_mdict, "stderr", PyDict_GetItemString(pyhp_mdict, "log"));

    if (pyhp_config_isdevel(r))
        pyhp_pydict_add_string(pyhp_mdict, "mode", "development");
    else
        pyhp_pydict_add_string(pyhp_mdict, "mode", "production");

#if 0
    /* Take a snap of the current modules state if in dev mode */
    if ( pyhp_config_isdevel(r) ) {
        PyInterpreterState *interp = PyThreadState_Get()->interp;
        PyObject *sys_modules = interp->modules;
        old_modules = PyDict_Keys(sys_modules);
    }
#endif

    pyhp_request_parse_params(pydata, r);
    pyhp_request_parse_headers(pydata, r);
    pyhp_cookies_parse(pydata, r);
    pyhp_session_load(pydata, r);

    pyhp_layer_set_object(pydata, "content_type", PyString_FromString(default_content_type));
    PyDict_SetItemString(pyhp_mdict, "compress", Py_False);

    result = pyhp_parse_file(pydata, r, r->filename);
    r->status = PyHP_RunPython(pydata, r, result);

    //Add Deflate filter if compression has been requested
    if (PyDict_GetItemString(pyhp_mdict, "compress") != Py_False)
        ap_add_output_filter("DEFLATE", NULL, r, r->connection);

    ap_set_content_type(r, PyString_AsString(pyhp_layer_get_object(pydata, "content_type")));

    pyhp_request_write_headers(pydata, r);
    pyhp_session_write(pydata, r);
    pyhp_cookies_write(pydata, r);
    pyhp_upload_clean(pydata, r);

#if 0
    /* Unload modules that were not previously loaded if in dev mode */
    if ( pyhp_config_isdevel(r) ) {
        PyInterpreterState *interp = PyThreadState_Get()->interp;
        PyObject *sys_modules = interp->modules;
        PyObject *key, *value;
        Py_ssize_t pos = 0;

        PyObject *to_delete = PyList_New(0);

        while (PyDict_Next(sys_modules, &pos, &key, &value)) {
            if (PyString_Check(key) && PyModule_Check(value)) {
                if (PySequence_Contains(old_modules, key) == 0) {
                    PyList_Append(to_delete, key);
                }
            }
        }

        for(pos = 0; pos < PyList_GET_SIZE(to_delete); ++pos) {
            key = PyList_GetItem(to_delete, pos);
            PyDict_DelItem(sys_modules, key);
        }

        Py_XDECREF(to_delete);
        Py_XDECREF(old_modules);
    }
#endif

    /* Revert previous configuration */
    memcpy(config, &old_config, sizeof(pyhp_config));

    return OK;
}

static void pyhp_register_hooks(apr_pool_t *p)
{
    ap_hook_child_init(pyhp_child_init, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_handler(pyhp_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

extern const command_rec pyhp_options[];

module AP_MODULE_DECLARE_DATA pyhp_module =
{
    STANDARD20_MODULE_STUFF,
    NULL,                   /* per-directory config creator */
    NULL,                   /* dir config merger */
    pyhp_make_config,       /* server config creator */
    NULL,                   /* server config merger */
    pyhp_options,           /* command table */
    pyhp_register_hooks,    /* set up other request processing hooks */
};
